Skip to content

feat(types): spec-generated REST/RELAY TypedDicts + wired return types + GEN-FRESH gate#39

Draft
mjerris wants to merge 42 commits into
mainfrom
experiment/rest-typeddict-generator
Draft

feat(types): spec-generated REST/RELAY TypedDicts + wired return types + GEN-FRESH gate#39
mjerris wants to merge 42 commits into
mainfrom
experiment/rest-typeddict-generator

Conversation

@mjerris

@mjerris mjerris commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator

What

Adds field-level static types to the REST/RELAY surface, generated mechanically from the canonical specs (the same approach TypeScript uses), with zero runtime change and zero cross-port drift.

41 commits · 263 files · +30.8k/−16.0k.

  • 14 generated modules (rest/namespaces/*_types_generated.py + relay/protocol_types_generated.py): one TypedDict(total=False) per components/schemas entry + per-operation Request/Response aliases. 1270 generated types (875 TypedDict + 395 TypeAlias). Emitted by porting-sdk/scripts/generate_python_rest_types.py from rest-apis/*/openapi.yaml + relay-protocol/*.json.
  • REST methods across 12 namespaces wired to their generated return types (e.g. fabric.list_versions() -> CallFlowVersionListResponse), guided by the TS port. Imported under TYPE_CHECKING with string forward-refs.
  • REST re-opened: create/update take closed typed params + an extras door + a **kwargs tail (the kwargs-language idiom), with reserved-word fields reachable via the _reserved_kw literal-key door. (This is the current head of the branch — the surface was briefly fully-closed mid-branch, then re-opened to the extras+kwargs model that matches the cross-port floor.)
  • GEN-FRESH gate in run-ci.sh: --check fails if any committed generated module no longer reproduces from its spec (mirror of TS's --check).

Runtime: unchanged

  • The generated module is imported only under TYPE_CHECKING — never loaded at runtime (verified: absent from sys.modules).
  • Method bodies are untouched; they still return the raw server JSON dict. A TypedDict is a plain dict at runtime, so a differently-shaped server response is returned unchanged and never raises (total=False + open shape).
  • Existing caller code (result["id"]) works identically. The types are additive IDE/mypy enrichment.

Drift: zero, complex types preserved

All ports stay drift=0. python and TS record the same named generated types (matched by leaf-name in the porting-sdk checker); the dict-recording ports (Go map[string]any, etc.) match via the checker's gen:X ≈ dict rule. No flattening to dict[str, Any] — the field-level types are kept.

Gates

mypy --config zero · ruff format + check clean · GEN-FRESH pass · all ports drift=0.

Dependency

Requires the porting-sdk changes (generator + checker normalization rules + GEN-FRESH script) on porting-sdk main first (#53), and the TS adapter alias-recording change (signalwire-typescript #140).

🤖 Generated with Claude Code

@mjerris mjerris marked this pull request as draft June 27, 2026 20:15
mjerris and others added 29 commits July 1, 2026 09:56
…n types + GEN-FRESH gate

Adds field-level static types to the REST/RELAY surface, generated mechanically
from the canonical specs (the same approach the TypeScript port uses), with zero
runtime change and zero cross-port drift.

- 15 generated modules (rest/namespaces/*_types_generated.py + relay/
  protocol_types_generated.py): one TypedDict per openapi/relay-protocol schema +
  per-operation Request/Response aliases. 1468 types. Emitted by
  porting-sdk/scripts/generate_python_rest_types.py.
- 160 REST methods across 20 namespaces wired to their generated return types
  (e.g. fabric list_versions -> CallFlowVersionListResponse), guided by the TS
  port. Imported under TYPE_CHECKING with string forward-refs: ZERO runtime cost
  (the generated module is never imported at runtime) and the method bodies are
  unchanged — they still return the raw server JSON dict. A TypedDict is a plain
  dict at runtime, so a differently-shaped server response is returned unchanged
  and never raises.
- GEN-FRESH gate in run-ci.sh: `generate_python_rest_types.py --check` fails if
  any committed generated module no longer reproduces from its spec (mirror of
  the TS port's --check).

Gates: mypy --config zero, ruff format + check clean, GEN-FRESH pass. All 9 ports
remain drift=0 (the complex types are matched cross-port by the porting-sdk
checker's generated-type normalization; dict-recording ports match via gen<->dict).

Depends on porting-sdk: generator + checker rules + GEN-FRESH script must be on
porting-sdk main first.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The 15 generated *_types_generated / protocol_types_generated modules add ~1500
type-only statements (imported solely under TYPE_CHECKING, never executed), which
dragged total coverage from ~74% to 55% and tripped the 63% fail-under. They carry
no testable runtime code and are policed by the GEN-FRESH gate, not by tests — omit
them from coverage measurement.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
WIP checkpoint (#81): CrudResource[TList,TItem,TCreate,TUpdate] generic base;
26 REST resources bound to spec-generated types; AutoMaterializedWebhook.create
honestly returns dict (intermediate orphan-create); coverage omits generated stubs.
mypy clean, deprecation tests pass, 9/9 ports drift=0. NOTE: ruff check still
flags forward-ref TYPE_CHECKING imports as unused (fix pending). Local checkpoint.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The generated-type imports under TYPE_CHECKING are used only inside quoted
forward-refs (return annotations + CRUD-base subscript bindings), which ruff's
F401 can't see -> false 'unused import' + --fix strips them, breaking mypy.
Scoped per-file-ignore (mypy + DRIFT gate prove the imports are real).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Prepares the REST client for generated typed CRUD resources.

- CrudResource.create/update: drop the broken `**kwargs: TCreate/TUpdate`
  (which typed each kwarg VALUE as a whole request dict) for an honest
  `**kwargs: Any -> TItem` fallback. The closed typed shape lives on the
  generated per-resource subclasses; the class-level CrudResource[...] binding
  (untouched) is what publishes the real TCreate/TUpdate to the oracle, so the
  structural crud_base{bind:[4]} is unchanged and all ports stay drift=0.
- Move FabricResource / FabricResourcePUT from the fabric namespace into _base
  (they are generic CRUD bases) so the generated fabric_resources_generated
  subclasses can inherit them without an import cycle. fabric.py re-imports
  them; they remain importable from there. These intermediate generics are not
  recorded in the oracle, so the move is drift-neutral.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF
…xtras)

Wire the generated per-resource typed CRUD subclasses into the fabric namespace
and update the tests for the now-enforced spec-required fields.

- fabric_resources_generated.py (generated): 12 typed CRUD subclasses, one per
  full-CRUD fabric resource, each bound to its spec types with a closed
  create/update — explicit spec fields (required honored) + an explicit `extras`
  door, no **kwargs tail. The named-subclass shape the oracle records as a
  crud_base; the structural binding is unchanged so parity holds.
- fabric.py: construct the generated subclasses (group-A: ai_agents, sip_gateways,
  the script/connector/endpoint families, webhooks) instead of bare
  FabricResource[...] instances. Remove AutoMaterializedWebhook + its deprecation
  warning — these SDKs are pre-release, so direct webhook create is just a normal
  operation (no back-compat to deprecate).
- tests: the typed create now enforces spec-required fields at the signature, so
  the coverage suites pass complete bodies (per-resource _CREATE_BODY/_UPDATE_BODY
  maps); error tests send a valid body so the 422 comes from the server, not the
  client; webhook tests assert no deprecation warning. Add
  TestRequiredFieldEnforcement: each missing required field raises TypeError, a
  typo'd field is rejected, and `extras` merges unknown fields into the body.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF
…ideo

Output of the x-sdk-resource-driven generator for the non-fabric CRUD namespaces,
plus a regenerated fabric module (PUT resources now carry _update_method = "PUT"
rather than extending a separate FabricResourcePUT base).

These modules are generated and mypy/ruff-clean but NOT yet wired into their
namespace files — wiring (and deletion of the now-fully-generatable hand classes)
follows once the per-resource sub-resource methods are generated too.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF
datasphere/relay-rest/video resource classes now include their declared
sub-resource methods (search, list_chunks, list_members, list_streams, etc.) in
addition to the base CRUD surface. Still generated-but-not-yet-wired; ruff + mypy
clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF
Each generated resource class carries an __init__ setting its base URL path
(namespace prefix + collection); construction is Resource(http).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF
…d-in paths

- queues.py now re-exports the generated QueuesResource (the hand class is fully
  covered by the generated CRUD + sub-methods).
- fabric.py: generated resource classes now bake their own base path into __init__,
  so construct them as Resource(http) (drop the path arg); the still-hand-written
  classes (call_flows/conference_rooms/subscribers/cxml_applications) keep (http, base).
- Regenerated resource modules with the collection-relative / sibling path fix.

All 766 rest tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF
Replace the hand-written resource classes with the generated ones (which cover the
full CRUD surface plus their declared sub-methods):
- queues / number_groups / verified_callers: re-export the generated class.
- datasphere: DatasphereNamespace constructs DatasphereDocumentsResource (back-compat
  alias DatasphereDocuments).
- video: rooms / conferences use VideoRoomsResource / VideoConferencesResource (aliases
  VideoRooms / VideoConferences); the other video resources stay hand-written.
- fabric: construct the generated resources as Resource(http) (they bake their own
  base path); hand group-B classes keep (http, base).

The generated resource modules and all wired namespace files are ruff + mypy clean and
GEN-FRESH passes. KNOWN: ~15 pre-existing CRUD tests fail because the generated typed
create/sub-methods enforce the spec's required fields and field names, while the tests
were written against the old **kwargs and used wrong/partial fields (e.g. phone_number
vs the spec's number). These are replaced by the generated tests in the next step; the
failures are stale tests, not wiring bugs (verified).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF
…roduct specs

- _base.py: add ReadResource (list + get); CrudResource extends it.
- logs.py: thin convenience namespace composing the generated per-product log
  resources (MessageLogsResource/VoiceLogsResource/FaxLogsResource from the
  messaging/voice/fax specs, ConferenceLogsResource from the logs spec). The hand
  classes are deleted; back-compat aliases kept.
- Generated read-only / method-only resource modules.

logs tests pass (read-only resources have no typed-create enforcement issue).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF
addresses/recordings/short_codes/sip_profile/imported_numbers/mfa/lookup now
generated (BaseResource + declared methods). Not yet wired.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF
Generated from the calling spec's CallRequest discriminator mapping; each command is a
typed method posting {command, params, id?}. Not yet wired.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF
The 7 call-routing convenience helpers (set_ai_agent/set_cxml_webhook/etc.) now
generated as typed wrappers over update(). Not yet wired.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF
registry (brands/campaigns/orders/numbers) and the project/chat/pubsub token
resources now generated; create ops with union bodies carry a typed body param.
Not yet wired.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF
fabric group-B sub-methods, GenericResources/FabricAddresses/FabricTokens/
CxmlApplications, and the video sub-resources (sessions/recordings/tokens/streams/
conference_tokens) now generated. Not yet wired.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF
…nd classes)

Replace the hand-written resource classes with the generated ones (which now cover the
full surface including sub-methods and phone_numbers' set_* helpers):
- addresses/recordings/short_codes/sip_profile/imported_numbers/lookup/mfa/
  phone_numbers re-export from relay_rest_resources_generated.
- chat/pubsub re-export from their generated modules.
- project: ProjectNamespace constructs ProjectTokensResource (back-compat alias
  ProjectTokens).

All routes resolve and methods are present (verified end-to-end). The remaining ~41
failing tests are all the benign typed-enforcement category (stale tests passing
wrong/partial field names to now-typed methods) — no wiring/route bugs; replaced by
generated tests in #10.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF
…nd classes)

- registry: RegistryNamespace constructs the 4 generated registry resources.
- calling: re-export the generated CallingResource (37 command-dispatch methods);
  back-compat alias CallingNamespace.
- video: VideoNamespace constructs all 7 generated video resources (rooms/conferences +
  room_tokens/room_sessions/room_recordings/conference_tokens/streams).
- fabric: FabricNamespace constructs the generated group-B (call_flows/conference_rooms/
  subscribers/cxml_applications) and root resources (resources/addresses/tokens); all
  hand resource classes deleted, back-compat aliases kept; list_addresses now routes the
  singular sub-path for call_flows/conference_rooms.

All routes resolve and methods/sub-methods are present (verified end-to-end). The
remaining failing tests are all stale tests of deleted hand-class behavior (typed-
enforcement kwargs, removed deprecation warnings, the removed cxml_applications.create
stub which never had a spec route) — no wiring/route bugs; replaced by generated tests
in #10.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF
Regenerate with bare class names (Queues, Subscribers, VideoRooms, ... — no Resource
suffix). Update the wiring (client.py + namespaces) and rest tests to the bare names.
Delete all 21 hand-written back-compat aliases; the one kept name, CallingNamespace, is
now a generated alias (x-sdk-resource.aliases on the calling resource).

Full rest suite failure count unchanged (98, all the pre-existing benign typed-
enforcement stale tests) — the rename introduced no new breakage. GEN-FRESH passes;
ruff + mypy clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF
…mespace alias

- PhoneCallHandler is now generated from the relay-rest spec's call-handler enum (13
  values, in sync with the wire) and re-exported from signalwire.rest; the hand-written
  call_handler.py (11 values) is deleted.
- calling: drop the CallingNamespace alias; the resource is `Calling` everywhere
  (client.py constructs Calling; tests updated).

Full rest suite failure count unchanged (98 — the pre-existing benign typed-enforcement
stale tests); no new breakage. ruff + mypy clean; GEN-FRESH passes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF
client.py now inherits the generated _GeneratedResourceTree and calls _wire_resources()
instead of 21 hand wiring lines — it owns only auth + the (pending-removal) compat
namespace. The full tree (client.queues, client.fabric.ai_agents, client.video.rooms,
client.logs.messages, ...) is generated from each resource's spec placement.

Full rest suite failure count unchanged (98 benign); no new breakage. ruff + mypy clean;
GEN-FRESH passes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF
…erated tree)

The hand namespace files (fabric.py/video.py/queues.py/...) — container classes and
resource re-exports — are fully superseded by the generated _client_tree + the
*_resources_generated modules, and nothing imports them anymore. Delete all 20; the one
test reference (test_client) now imports the containers/Calling from the generated
modules. namespaces/ is now generated modules + compat (the deliberate hand exception).

Full rest suite failure count unchanged (98 benign); no new breakage. ruff + mypy clean;
GEN-FRESH passes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF
…rface

Align the REST tests to the generated SDK surface (resources are now generated from the
specs). Done via a 24-agent test-fix pass + the two generator fixes:
- Wrong/renamed fields -> the spec's actual field name (domain->domain_identifier,
  friendly_name->name, code->verification_code, destination->dest, phone_number->number,
  token->refresh_token, embed_id->token, name->display_name, ...).
- Partial bodies in error tests -> the spec's required fields with valid placeholders
  (the 4xx still comes from the pushed mock scenario).
- Removed hand behavior -> removed obsolete deprecation-warning assertions and the
  cxml_applications.create stub (no spec route).
- Generator-surfaced fields now reachable: Mfa.sms(from_), Calling.dial(to/from_/url/
  codecs/...), Calling.update(id/status) — tests updated to the typed surface (from_
  param -> wire "from"; status not state).
- Addresses.create now sends the 9 spec-required fields.

Full rest suite: 765 passed (was 98 failing). No spec edits — real spec gaps were
logged (the SPEC_AUDIT_vs_RAILS.md findings), none papered over.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF
…mypy --strict

Annotate the last non-generated rest code — the runtime substrate the generated
resources sit on:
- _base.py: HttpClient (_request/get/post/put/patch/delete -> Any wire JSON), BaseResource
  (__init__/_path), and the CRUD base methods (cast the Any wire returns to the bound
  TList/TItem). SignalWireRestError.__init__.
- _pagination.py: PaginatedIterator fully annotated (also merged the stray double module
  docstring so imports sit at the top).
- client.py: RestClient.__init__ signature.
- core/logging_config.py: get_logger(name: str) -> Any (it was untyped and used by 31
  files; fixed at the source rather than ignored in rest).
- regenerated relay-rest (set_* helpers drop the now-redundant cast).

Result: mypy --strict is clean across all 31 generated + wiring + substrate rest files
(compat.py excluded — it is being removed in the Twilio-compat removal). The configured
project mypy stays clean; the 765 rest tests still pass; ruff clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF
…, tests)

Delete the compat surface completely:
- namespaces/compat.py (the 12 Compat* hand classes) and
  namespaces/compatibility_types_generated.py.
- client.py: drop the CompatNamespace import + the self.compat construction; RestClient
  is now purely the generated resource tree + auth.
- tests: delete all 15 test_compat_*.py files and the TestCompat class in
  test_namespaces.py; drop the compat references/examples in test_client.py and conftest.
- _base.py: drop the compat-subclass rationale from the update() comment (the positional-
  only resource_id stays — harmless and keeps the oracle signature stable).

Full rest suite: 506 passed (the ~259 compat tests removed). No functional compat remains
anywhere; mypy --strict clean (30 files); GEN-FRESH passes. The only residual "compat"
mentions are generated wire-description docstrings (laml_call value sources), which are
correct.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF
…n); rest: regenerate

- relay/call.py: collapse the ~14 repeated stop/pause/resume/volume action sub-methods
  into reusable bases parameterized by a _command_prefix class attr — StoppableAction
  (stop), PausableAction (+pause/resume), VolumeAction (+volume). Each *Action class now
  just declares its prefix + composes the right base (FaxAction sets the prefix per
  instance for send/receive). Behaviour preserved; the test asserting the old
  _method_prefix attr updated to _command_prefix.
- rest: regenerate (no surface change from the BaseResource/default/positional generator
  work — default stays doc-only so the wire body is unchanged).

mypy --strict clean on call.py + the generated modules; rest + relay suites green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF
…b typing)

SWMLBuilder inherits the generated _SwmlVerbs Protocol under TYPE_CHECKING, so the verb
methods it installs dynamically (from schema.json) are now statically typed — clears the
verb-visibility strict errors (swml_builder 21 -> 7; remaining are the dynamic-creation
internals, hand-annotation territory). Runtime behavior unchanged (Protocol is type-only);
416 SWML/builder tests + the full core/rest/relay suites green.

Adds signalwire/core/swml_verbs_generated.py (generated from porting-sdk/schema.json).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF
…ld fix these

A targeted pass over strict errors that no spec-generator would address:
- abstract SWMLBuilder regression fixed (the _SwmlVerbs base is now a plain class with
  concrete bodies — see the generator commit).
- Explicit re-exports (__all__) on _agent_host / _mixin_host so --strict no_implicit_reexport
  resolves AgentHost/_HostTyped (cleared the 9 attr-defined on the mixins).
- type-arg (32 -> 0): bare dict/list/set/tuple/Callable/Pattern given their args across 16
  files; auth_mixin gained its missing `from typing import Any`.
- logging_config.py fully annotated (strip_control_chars/get_execution_mode/configure_logging/
  the structlog processor + mode helpers) — a foundational file used by 31 modules, so its
  typed calls cascade-cleared a chunk of no-untyped-call package-wide.

The remaining 238 are genuine framework-typing work (FastAPI route handlers + @tool/@app
decorators + the deliberate _HostTyped TYPE_CHECKING/runtime-split tradeoff) — not
spec-generatable, left for a dedicated framework-annotation effort. ruff clean; 5358 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF
mjerris and others added 12 commits July 1, 2026 09:56
…trict as the gate

A 5-agent parallel burndown of the remaining strict errors, with REAL types — never
papered with bare Any (dict[str,Any] only where the value is genuinely heterogeneous, the
honest equivalent of TS's Record<string,unknown>). Corrects an earlier wrong claim that
this framework code "couldn't be typed/generated": TypeScript passes full strict on the
same surface (web/agent/swml) with only 3 `any` in its src, so it clearly can.

Key fixes:
- The @AgentBase.tool() decorator is now properly typed (TypeVar bound to Callable),
  clearing untyped-decorator cascades SDK-wide. The untyped-decorator errors were NOT
  structural — they were a missing-return-annotation cascade (proved: a typed-return
  FastAPI handler is clean).
- FastAPI route-handler return annotations are RUNTIME-LOAD-BEARING (FastAPI builds a
  response model from them; a union return crashes startup). Resolved with an _as_response
  helper: internal _handle_* methods honestly return Response | dict[str, Any] (the dict is
  a real SWAIG/post-prompt passthrough that tests assert on); decorated route handlers
  funnel through _as_response to stay -> Response. No behavior change.
- Explicit __all__ re-exports (_agent_host/_mixin_host) for no_implicit_reexport; the
  _HostTyped "cannot subclass Any" is the documented TYPE_CHECKING/runtime split -> scoped
  # type: ignore[misc] with reason. flask/flask-limiter (optional extras, no stubs) ->
  scoped # type: ignore[untyped-decorator].
- type-arg/redundant-cast cleared; logging_config fully annotated (cascade-cleared callers).
- service_loader: bytes(result.body).decode() — a real latent bug (memoryview has no
  .decode()) that strict surfaced.

Config: tool.mypy now `strict = true` (was a hand-picked subset) so the gate matches the
bar. `python -m mypy` passes at 0; ruff clean; 5358 unit tests pass.

Note: tests/ are NOT yet in the type-check scope (76k LOC, ~9570 strict errors) — deferred
to the test-generation effort (#10) which will replace much of that surface with generated,
already-typed code rather than hand-typing throwaway. Pre-existing broken example
(bedrock_agent_run.py imports a never-defined run_agent) is unrelated, left as-is.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF
When the REST layer became spec-generated, 20 hand namespace modules were deleted and the
resource classes lost their *Resource/*Namespace suffixes (PhoneNumbersResource ->
PhoneNumbers, CallingNamespace -> Calling, ...). client.<ns>.<resource> usage was
unaffected, but direct imports broke:
  from signalwire.signalwire.rest.namespaces.phone_numbers import PhoneNumbersResource
  from signalwire.signalwire.rest.call_handler import PhoneCallHandler

Re-add the 20 namespace modules + call_handler.py as thin RE-EXPORT STUBS (old name ->
generated bare name) so those imports keep working. Each carries the
`x-sdk-back-compat-shim` marker so the cross-port surface oracle skips it — these are
PYTHON-ONLY and must NOT be added to other ports. (client.compat is deliberately NOT
shimmed — that API was intentionally removed. AutoMaterializedWebhook has no equivalent
and is not re-exported.)

506 rest tests pass; ruff + mypy clean; the shims add ZERO oracle surface (verified
byte-identical with/without).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF
Each Python-only shim (the 20 namespace re-export stubs + call_handler.py) now warns on
import that the path is deprecated, pointing at client.<ns> (the supported access). Old
imports keep working; normal client.<ns>.<resource> usage emits NO warning (the client tree
never imports the shims). Message is path-accurate — it recommends client.<ns> rather than
naming a per-symbol module (FabricResource lives in _base, others in *_resources_generated;
client.<ns> is correct for all). Shims stay oracle-invisible (x-sdk-back-compat-shim).

506 rest tests pass; ruff clean; client.* usage verified warning-free.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF
…enai spec)

swaig_actions_generated.py — 27 typed action builders + the <Action> value TypedDicts,
generated from porting-sdk/swaig-specs/swaig-response.yaml (vendored from mod_openai). The
ergonomic FunctionResult methods will compose this typed layer. ruff + mypy --strict clean;
runtime-verified to build correct wire output.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF
…equest

swaig_request_generated.py — the SwaigRequest TypedDict (+ SwaigArgument), generated from
porting-sdk/swaig-specs/swaig-request.yaml (vendored from mod_openai). swaig_function.execute
now takes raw_data: "SwaigRequest | None" (TYPE_CHECKING import — no runtime cost/cycle; a
plain dict at runtime), replacing the bare dict[str, Any]. The inbound function-webhook
payload is now statically typed where the handler receives it.

ruff + mypy --strict clean; 1815 core tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF
post_prompt_generated.py — the full post-prompt callback payload tree (PostPrompt envelope +
the call_log role-union + swaig_log/times + nested sub-objects), generated from
porting-sdk/swaig-specs/post-prompt.yaml (vendored from mod_openai). agent_base:
- on_summary(summary: PostPromptData | None, raw_data: PostPrompt | None) — the user callback
  now receives a typed payload.
- _find_summary_in_post_data(body: PostPrompt) — typed; the post_prompt_data.parsed/.raw reads
  type-check against the corrected extract_json shape.

Behavior unchanged (annotations only; the legacy top-level `summary` fallback is preserved via
an untyped probe). TYPE_CHECKING imports (no runtime cost/cycle). mypy --strict clean; 1815
tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF
…ests

Add the 19 generated <ns>_generated_test.py files (422 tests: success + error per route,
asserting method/matched_route against the mock) and delete the 20 superseded hand
test_*_full_mock.py files. The generated suite covers every route the hand full-mock tests
covered (178) plus 31 more, captured from the real client + spec operationIds.

Behavioral hand tests (pagination, response parsing, client construction, the per-namespace
test_<ns>.py) are KEPT — they assert things beyond wire shape. 613 REST tests pass; 5465 unit
tests pass overall. Generated files are ruff + mypy --strict clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF
…ests in the gate

Drove mypy --strict to zero across the entire tests/ tree (was ~8950 errors) under the REAL
whole-tree config, and added `tests` to [tool.mypy] files so the TYPECHECK gate now covers
tests too (a new untyped test fails CI).

- generated REST tests are strict-clean by construction; hand tests fully annotated with REAL
  types (-> None, typed fixtures, typed handlers) — never blanket Any.
- mock-monkeypatch sites (mock.method = ...) carry # type: ignore[method-assign] + reason;
  intentional invalid-input tests carry # type: ignore[arg-type] + reason; stale ignores from
  an earlier --ignore-missing-imports pass removed (warn_unused_ignores keeps them honest).
- real fixes surfaced by the strict pass: union-attr None-narrowing (assert), override-sig
  alignment, ClassVar/tuple/None-callable, generic type-args.
- conftest.py + the 3 top-level tests/*.py fully typed; AgentBase imported from its canonical
  module so mypy resolves it.

Source fix: prefabs (survey/receptionist/faq_bot/concierge) on_summary overrides updated to the
new PostPromptData/PostPrompt base signature — the post-prompt retype had made them incompatible
overrides (the whole-tree run caught this; source TYPECHECK would have gone red).

Verified: `mypy --config-file pyproject.toml` = 0 across 338 files; 5523 unit tests pass (the
one bedrock-example failure is pre-existing — imports a nonexistent run_agent).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF
…g is a fatal call error

The SWML `ai` verb's `prompt` must be an OBJECT — {"text": ...} or {"pom": [...]}. A bare string
is a FATAL error in the AI engine: mod_openai app_config.c does `if (!cJSON_IsObject(prompt))`
-> fires calling.error and aborts the call (verified against mod_infrastructure + mod_openai
source, not just the SDK schema). SWMLBuilder.ai() emitted `config["prompt"] = prompt_text`
(bare string) — and likewise post_prompt. Now wraps text -> {"text": ...}, pom -> {"pom": ...},
post_prompt -> {"text": ...}, matching the production AgentBase path (agent_base.py:1294) and
the engine.

Latent until now because the production render path builds the wrapped object itself and
bypasses SWMLBuilder.ai; only direct builder / SwmlRenderer.render_swml(str) callers hit it.

- update test_swml_builder assertions to the object form (they enshrined the bare-string bug
  via a mock service that skips schema validation).
- render_swml tests now pass real `str`/`pom` args (11 of 12 # type: ignore[arg-type] dropped;
  the remaining one is a legit None invalid-input test) — proving str -> valid SWML end-to-end.

Cross-port: flagged in porting-sdk GLOBAL_MEMORY (Go already fixed; .NET still has it; Contexts
path is the OPPOSITE contract — bare string is correct there, do not "fix").

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF
…pdate)

Regenerated the REST resource modules — create/update/operation methods are now fully closed to
the spec fields (no `extras` param). Reserved-word fields like `from` are exposed as the typed
kwarg `from_` (mapped back to the wire key), so nothing is lost.

- test_small_namespaces_mock: mfa.call now passes from_="..." instead of extras={"from":...}.

mypy --strict clean (338 files); 5466 unit tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF
…l-key door

Methods whose wire body has a reserved-word field (e.g. mfa.call's `from`) now expose both the
typed `from_` param AND a `**_reserved_kw` tail, so callers can pass the literal wire key via
`**{"from": ...}`. The `_reserved_kw` tail is dropped by the signature oracle (Python-only
workaround; surface stays closed cross-port).

mypy --strict clean; 613 REST tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF
… create/update)

create/update/operation methods carry the typed spec fields + extras + **kwargs again. The
override-ignore is now emitted only where the signature genuinely narrows the base (required-
param create / positional-body), so warn_unused_ignores stays satisfied.

mypy --strict clean (338 files); 613 REST tests pass; 5466 unit tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwtz9te6ivK5WgSJ61zXAF
@mjerris mjerris force-pushed the experiment/rest-typeddict-generator branch from e6ddefc to 09e3168 Compare July 1, 2026 13:57
… scheduler

run-ci gates now run through porting-sdk/scripts/gate_scheduler.sh: pure-Python
side-effect-free gates run CONCURRENTLY (S2), heavy gates (TEST/build) deferred behind
the cheap wave (S1 fail-fast). Data-deps honored (DRIFT deps=SIGNATURES; surface-mutating
gates share res=surface); only genuinely-contending gates serialized (java res=gradle) —
schedule-freely, throttle only what actually breaks. Per-gate PASS/FAIL + FAILED_GATES
tally preserved; opt-in --fail-fast (default = full report). Gate set byte-identical to
before (no gate dropped/added). Measured warm: ts 37->20s, go 17->5s, rust 94->30s;
injected cheap-gate failure --fail-fast fails in ~0-2s vs full serial run.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H8UmLE1BTztLFXspwbJWjj
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant